﻿using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Sustainalytics.Entities;
using Sustainalytics.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace Sustainalytics.DataLayer.Shared
{
    public class PropertyValuePair<T, TK>
    {
        private readonly Expression<Func<T, TK>> _property;
        private readonly TK _newValue;

        public PropertyValuePair(Expression<Func<T, TK>> property, TK newValue)
        {
            _property = property;
            _newValue = newValue;
        }

        public Expression<Func<T, TK>> Property
        {
            get { return _property; }
        }

        public TK NewValue
        {
            get { return _newValue; }
        }
    }

    public class RepositoryCollection<T> : RepositoryCollection<T, Guid>, IRepositoryCollection<T> where T : IEntity
    {
        public RepositoryCollection(string mongoDbUrl, string collectionName = null)
           : base(mongoDbUrl, collectionName)
        {
        }

        public RepositoryCollection(string mongoDbUrl, int writeConcern, TimeSpan writeConcernTimeOut, string collectionName = null)
            : base(mongoDbUrl, collectionName)
        {
        }
    }

    public class RepositoryCollection<T, TKey> : IRepositoryCollection<T, TKey> where T : IEntity<TKey>
    {
        protected readonly IMongoCollection<T> _collection;
        protected readonly IMongoCollection<T> _newCollection;
        protected readonly string _collectionName;
        protected readonly string _dbName;
        private MongoUrl _url;

        /// <exception cref="ArgumentException">mongoDbUrl or database name</exception>
        public RepositoryCollection(string mongoDbUrl, string collectionName = null)
        {
            _collectionName = string.IsNullOrEmpty(collectionName) ? typeof(T).Name : collectionName;

            if (string.IsNullOrEmpty(mongoDbUrl))
            {
                throw new ArgumentException("mongoDbUrl");
            }

            _url = new MongoUrl(mongoDbUrl);
            if (string.IsNullOrEmpty(_url.DatabaseName))
            {
                throw new ArgumentException("databaseName");
            }


            var mongoClient = new MongoClient(mongoDbUrl);
            var database = mongoClient.GetDatabase(_url.DatabaseName);
            _collection = database.GetCollection<T>(_collectionName);
            _dbName = _url.DatabaseName;


            var mongoUrlBuilder = new MongoUrlBuilder(mongoDbUrl);
            var mongoClientSettings = MongoClientSettings.FromUrl(mongoUrlBuilder.ToMongoUrl());
            mongoClientSettings.WaitQueueSize = int.MaxValue;
            mongoClientSettings.MinConnectionPoolSize = 1;
            mongoClientSettings.MaxConnectionPoolSize = 100;

            var newMongoClient = new MongoClient(mongoClientSettings);
            _newCollection = newMongoClient.GetDatabase(mongoUrlBuilder.DatabaseName).GetCollection<T>(_collectionName);
        }

        public IMongoCollection<T> Collection()
        {
            return _collection;
        }

        public virtual bool Drop()
        {
            _collection.Database.DropCollection(_collectionName);
            return true;
        }

        public string GetCollectionName()
        {
            return _collectionName;
        }

        public string GetDatabaseName()
        {
            return _dbName;
        }

        public bool DropAndCreate()
        {
            try
            {
                if (!CollectionExists())
                {
                    return true;
                }

                _collection.Database.DropCollection(_collectionName);
                _collection.Database.CreateCollection(_collectionName);

                return true;
            }
            catch (RepositoryRemoveAllException ex)
            {
                RepositoryLog.Log("RemoveAll", "", ex, _dbName, _collectionName);
                throw;
            }
        }

        public virtual bool Create(T entity)
        {
            _collection.InsertOne(entity);
            RepositoryLog.Log("Insert", null, "", _dbName, _collectionName);
            return true;
        }

        public bool CreateBatch(IEnumerable<T> toInsert)
        {
            return CreateBatchIntenal(toInsert, _collection);
        }

        private bool CreateBatchIntenal(IEnumerable<T> toInsert, IMongoCollection<T> collection)
        {
            collection.InsertMany(toInsert);
            RepositoryLog.Log("InsertMany", null, "", _dbName, _collectionName);
            return true;
        }

        public bool CreateBatchParallel(IEnumerable<T> toInsert, int bulkInsertSize = 1000, int maxParalelism = -1)
        {
            if (toInsert == null)
                throw new ArgumentNullException("toInsert");

            if (!toInsert.Any())
                throw new ArgumentException("toInsert");

            if (toInsert.Count() <= bulkInsertSize)
                return this.CreateBatch(toInsert);

            var returnVal = true;
            var p = Partitioner.Create(0, toInsert.Count(), bulkInsertSize);
            var options = new ParallelOptions { MaxDegreeOfParallelism = maxParalelism == -1 ? Environment.ProcessorCount : maxParalelism };

            Parallel.ForEach(p, options, (range, loopState) =>
            {
                var subset = toInsert.Skip(range.Item1).Take(bulkInsertSize);
                var mongoClient = new MongoClient(_url);
                var database = mongoClient.GetDatabase(_url.DatabaseName);
                var collection = database.GetCollection<T>(_collectionName);
                if (!CreateBatchIntenal(subset, collection))
                    returnVal = false;
            });

            return returnVal;
        }

        public virtual bool CreateOrUpdate(T entity)
        {
            var result = _collection.FindOneAndReplace(
                Builders<T>.Filter.Eq(x=> x.Id, entity.Id),
                entity,
                new FindOneAndReplaceOptions<T>() { IsUpsert = true });
            RepositoryLog.Log("Save", null, "", _dbName, _collectionName);
            return true;
        }

        public virtual bool CreateOrUpdateBatch(IEnumerable<T> toInsert)
        {
            foreach (var item in toInsert)
            {
                _collection.ReplaceOne(Builders<T>.Filter.Eq(x => x.Id , item.Id), item, new UpdateOptions { IsUpsert = true });
            }
            RepositoryLog.Log("CreateOrUpdateBatch", null, true, _dbName, _collectionName);
            return true;
        }

        public virtual bool Delete(TKey id)
        {
            var result = _collection.DeleteOne(Builders<T>.Filter.Eq(x => x.Id, id));
            RepositoryLog.Log("Remove", id, result.IsAcknowledged.ToString(), _dbName, _collectionName);
            return result.DeletedCount > 0;
        }

        public void Delete(IEnumerable<TKey> ids)
        {
            var enumerable = ids as TKey[] ?? ids.ToArray();

            if (enumerable.Count() == 1)
            {
                Delete(enumerable.First());
                return;
            }

            var result = _collection.DeleteMany(Builders<T>.Filter.In(x => x.Id, enumerable));
            RepositoryLog.Log("Remove", enumerable.Count(), result.IsAcknowledged.ToString(), _dbName, _collectionName);
        }

        public virtual bool DeleteWhere(Expression<Func<T, bool>> wherePredicate)
        {
            if (wherePredicate == null)
            {
                throw new ArgumentNullException("wherePredicate");
            }

            var result = _collection.DeleteMany(Builders<T>.Filter.Where(wherePredicate));
            RepositoryLog.Log("RemoveWhere", null, result.IsAcknowledged.ToString(), _dbName, _collectionName);
            return result.DeletedCount > 0;
        }

        public virtual bool DeleteAll()
        {
            var result = _collection.DeleteMany(_ => true);
            RepositoryLog.Log("RemoveAll", null, result.IsAcknowledged.ToString(), _dbName, _collectionName);
            return true;
        }

        public virtual T Read(TKey id)
        {
            var result = _collection.Find(Builders<T>.Filter.Eq(x=> x.Id, id)).FirstOrDefault();
            RepositoryLog.Log("FindOneByIdAs", id, result, _dbName, _collectionName);
            return result;
        }

        public virtual bool Exists(TKey id)
        {
            var result = _collection.Count(Builders<T>.Filter.Eq(x=> x.Id, id)) == 1;
            RepositoryLog.Log("Count(id)", id, result, _dbName, _collectionName);
            return result;
        }

        public virtual IList<T> ReadAll()
        {
            return ReadAll(null);
        }

        public virtual IList<T> ReadAll(string[] includedFields, int? skip = null, int? take = null, Ordering<T> ordering = null, bool useCollation = false)
        {
            var cursor = _collection.Find(_ => true);

            if (ordering != null)
            {
                cursor.Sort(ordering.ToMongoSortBy());
            }
            if (skip.HasValue && take.HasValue)
            {
                cursor.Skip(skip.Value);
                cursor.Limit(take.Value);
            }
            if (includedFields != null)
            {
                cursor.Project(Builders<T>.Projection.Include(includedFields));
            }

            return cursor.ToList();
        }

        //public virtual IList<T> ReadAllWhere(Expression<Func<T, bool>> wherePredicate, int? skip = null, int? take = null, Ordering<T> ordering = null)
        //{
        //    var queryable = _collection.AsQueryable().Where(wherePredicate);

        //    if (ordering != null)
        //    {
        //        //queryable.OrderBy(ordering.ToMongoSortBy());
        //    }
        //    if (skip.HasValue)
        //    {
        //        queryable = queryable.Skip(skip.Value);
        //    }
        //    if (take.HasValue)
        //    {
        //        queryable = queryable.Take(take.Value);
        //    }

        //    return queryable.ToList();
        //}

        public virtual IList<T> ReadAllWhere(Expression<Func<T, bool>> wherePredicate, int? skip = null, int? take = null, Ordering<T> ordering = null, bool useCollation = false,
            params string[] includeFields)
        {
            if (wherePredicate == null)
            {
                throw new ArgumentNullException(nameof(wherePredicate));
            }

            IFindFluent<T, T> cursor = null;

            if (useCollation)
                cursor = _collection.Find(wherePredicate, new FindOptions { Collation = new Collation(locale: "en", caseLevel: true, strength: CollationStrength.Primary) });
            else
                cursor = _collection.Find(wherePredicate);

            if (includeFields.Any())
            {
                cursor.Project(Builders<T>.Projection.Include(includeFields));
            }
            if (skip.HasValue)
            {
                cursor.Skip(skip.Value);
            }
            if (take.HasValue)
            {
                cursor.Limit(take.Value);
            }
            if (ordering != null)
            {
                cursor.Sort(ordering.ToMongoSortBy());
            }

            return cursor.ToList();
        }

        public IList<T> ReadWhere(Expression<Func<T, bool>> wherePredicate, int? skip = null, int? take = null, Ordering<T> ordering = null,
            params Expression<Func<T, object>>[] includeFields)
        {
            return ReadAllWhere(wherePredicate, skip, take, ordering, includeFields: includeFields.Select(x => x.GetPropertyName()).ToArray());
        }

        //TODO Petre,Madalina Remove this method. See ReadWhere for ref
        public IList<T> ReadAllWhereFieldEqualsValue<TK>(string fieldName, TK value, int? skip, int? take, string[] includedFields = null)
        {
            var cursor = _collection.Find(Builders<T>.Filter.Eq(fieldName, BsonValue.Create(value)));

            if (includedFields != null)
            {
                cursor.Project(Builders<T>.Projection.Include(includedFields));
            }
            if (skip.HasValue)
            {
                cursor.Skip(skip.Value);
            }
            if (take.HasValue)
            {
                cursor.Limit(take.Value);
            }

            return cursor.ToList();
        }

        //TODO Petre, Madalina Remove this method. See ReadWhere for ref
        public IList<T> ReadAllWhereFieldEqualsValue<TK>(Expression<Func<T, TK>> selectPropertyExpression, TK value,
            int? skip = null, int? take = null, string[] includedFields = null)
        {
            var propertyName = selectPropertyExpression.GetPropertyName();
            if (propertyName.IsNullOrEmpty())
            {
                throw new InvalidExpressionException();
            }
            return ReadAllWhereFieldEqualsValue(propertyName, value, skip, take, includedFields);
        }

        public IList<T> ReadAllWhereFieldEqualsValue<TK, TJ>(string firstFieldName, TK firstValue,
            string secondFieldName, TJ secondValue, int? skip, int? take, string[] includedFields = null)
        {
            var q = Builders<T>.Filter.And(
                Builders<T>.Filter.Eq(firstFieldName, BsonValue.Create(firstValue)),
                Builders<T>.Filter.Eq(secondFieldName, BsonValue.Create(secondValue)));

            var cursor = _collection.Find(q);

            if (includedFields != null)
            {
                cursor.Project(Builders<T>.Projection.Include(includedFields));
            }
            if (skip.HasValue)
            {
                cursor.Skip(skip.Value);
            }
            if (take.HasValue)
            {
                cursor.Limit(take.Value);
            }

            return cursor.ToList();
        }

        public IDictionary<TKey, T> ReadbyIds(IEnumerable<TKey> ids, string[] includedFields = null)
        {
            var cursor = _collection.Find(Builders<T>.Filter.In(x => x.Id, ids));

            if (includedFields != null)
            {
                cursor.Project(Builders<T>.Projection.Include(includedFields));
            }

            return cursor.ToList().ToDictionary(x => x.Id);
        }

        public long ReadCount()
        {
            var result = _collection.Count(_ => true);
            RepositoryLog.Log("FindAllAs", null, result, _dbName, _collectionName);
            return result;
        }

        public long ReadCount(Expression<Func<T, bool>> wherePredicate)
        {
            var result = _collection.Count(wherePredicate);
            RepositoryLog.Log("Count(IQuery)", null, result, _dbName, _collectionName);
            return result;
        }

        public bool Update(TKey id, T entity)
        {
            var result = _collection.FindOneAndReplace(
                Builders<T>.Filter.Eq(x => x.Id, id), 
                entity, 
                new FindOneAndReplaceOptions<T> {
                    IsUpsert = true,
                    ReturnDocument = ReturnDocument.After
                });

            RepositoryLog.Log("Save", null, result, _dbName, _collectionName);
            return result.Id.Equals(entity.Id);
        }

        public override string ToString()
        {
            return string.Format("{0}/{1}", _dbName, _collectionName);
        }

        public bool CollectionExists()
        {
            var filter = new BsonDocument("name", _collectionName);
            return _collection.Database.ListCollections(new ListCollectionsOptions { Filter = filter }).Any();
        }
    }
}
